<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>使用 React Native 构建 App</title>
    <link rel="stylesheet" href="./script/reveal.css" />
    <link rel="stylesheet" href="./script/theme/black.css" />
    <link rel="stylesheet" href="./main.css" />

    <style>
      .navbar-brand {
        color: #fff;
        text-decoration: none;
        display: inline-flex;
        font-weight: 700;
        height: 2rem;
        align-items: center;
        margin-right: 1rem;
      }
      .navbar-brand:hover {
        color: #61dafb;
      }
      .navbar-brand:hover .navbar-logo {
        transform: rotate(180deg) scale(0.9);
      }
      .navbar-logo {
        width: 38px;
        height: 34px;
        transition: transform 0.5s;
      }
      .head-intro strong {
        font-weight: 600;
        font-size: 18px;
        transition: color 0.5s;
        margin-left: 1.5rem;
      }
    </style>
  </head>
  <body>
    <div class="head-intro">
      <a class="navbar-brand" href="//reactnative.dev/"
        ><img
          class="navbar-logo"
          src="./images/header_logo.svg"
          alt="React Native"
        /><strong class="navbar-title">React Native</strong></a
      >

      <strong
        class="meet-title ab"
        style="left: 0; top: 50%; transform: translateY(-50%)"
        >Meet</strong
      >
    </div>

    <div class="reveal">
      <small class="copyright"
        >Member: Innei Jiang, Lomogo Si, Pan Bin, Liu Qiang</small
      >
      <small class="acknowledgement"> Powered by reveal.js </small>
      <div class="slides">
        <section>
          <section>
            <h1>「<del>遇见</del>一言 APP」</h1>
          </section>
          <section>
            <h1>开始之前, 聊一聊混合开发</h1>
          </section>

          <section>
            <h1>使用 React Native 构建 App</h1>
            <h4 style="text-align: right">-- 「<del>遇见</del>一言 APP」</h4>
          </section>

          <section>
            <h2>什么是混合开发</h2>
            <p>混合开发对应的是开发混合模式的应用程序 (hybrid application).</p>
            <p>
              hybrid app 是指介于web-app、native-app这两者之间的app，兼具“Native
              App良好用户交互体验的优势”和“Web
              App跨平台开发的优势”。混合应用本质上是被放在原生应用外壳中的网络应用。一旦它们从应用商店下载并安装在本地，外壳就能够通过嵌入应用中的浏览器连接到移动平台提供的任何功能。
            </p>
          </section>

          <section data-auto-animate>
            <h2>混合开发</h2>
            <p>
              混合开发的出现让编写 App 变得尤为简单.
              甚至能用一套代码同时编译生成 Android/iOS/Web 三端.
              更为激进的是如果使用 uni-app 一类框架能同时生成
              Android/iOS/Web/以及各类小程序平台.
            </p>
            <p>
              同时, 开发 Android App 不再受限于 Java 或 Kotlin, 开发 iOS
              不再受限于 Objective-C/C++ 或 Swift.
            </p>
            <p>让广大前端开发者参与到移动端开发中, 成为可能</p>
            <h3 class="tl">广为人知的混合开发框架</h3>
            <div class="images">
              <div class="ic">
                <img src="./images/header_logo.svg" alt="" height="80" />
                <caption>
                  React Native
                </caption>
              </div>

              <div class="ic">
                <img src="./images/flutter-logo.svg" alt="" height="80" />
                <caption>
                  Flutter
                </caption>
              </div>

              <div class="ic">
                <img src="./images/uniapp4@2x.png" alt="" height="80" />
                <caption>
                  uni-app
                </caption>
              </div>
            </div>
          </section>

          <section data-auto-animate>
            <h2>混合开发</h2>

            <div class="g g-half">
              <div class="">
                <span> 好处 </span>
                <br />
                <br />
                <ul>
                  <li>开发门槛低</li>
                  <li>开发成本低</li>
                  <li>编译速度更快</li>
                  <li>更容易做到热更新</li>
                </ul>
              </div>
              <div class="">
                <span> 缺点 </span>
                <br />
                <br />
                <ul>
                  <li>runtime 效率没有原生快</li>
                  <li>UI/UX 体验可能不及 native app</li>
                </ul>
              </div>
            </div>
          </section>

          <section data-auto-animate>
            <h2>混合开发</h2>

            <h4 class="tl">React Native</h4>

            <p>
              随着 React 的盛行，其移动开发框架 React Native
              也收到了广大开发者的青睐，以下简称 RN。通过 RN 我们能够使用
              JavaScript
              语言来实现跨平台移动应用的开发，打开了前端工程师通往移动平台的大门。用
              RN 官方的介绍来概括它的特点就是：<code>
                Learn once, write anywhere </code
              >。
            </p>
          </section>

          <section>
            <h2>React Native 混合开发</h2>

            <h4 class="tl">使用 React Native 构建的 APP</h4>
            <div class="images">
              <div class="ic">
                <figure>
                  <img src="images/facebook.png" alt="" height="80" />
                  <figcaption>
                    <p>Facebook</p>
                    <p>iOS/Android</p>
                  </figcaption>
                </figure>
              </div>

              <div class="ic">
                <figure>
                  <img src="images/qq.png" alt="" height="80" />
                  <figcaption>
                    <p>Tencent QQ</p>
                    <p>Android</p>
                  </figcaption>
                </figure>
              </div>
              <div class="ic">
                <figure>
                  <img src="images/discord.png" alt="" height="80" />
                  <figcaption>
                    <p>Discord</p>
                    <p>iOS</p>
                  </figcaption>
                </figure>
              </div>
              <div class="ic">
                <figure>
                  <img src="images/jdcom.png" alt="" height="80" />
                  <figcaption>
                    <p>JD</p>
                    <p>iOS/Android</p>
                  </figcaption>
                </figure>
              </div>
            </div>

            <div style="position: absolute; opacity: 0.4; font-size: 0.8em">
              Visit this to learn more:
              <a href="https://reactnative.dev/showcase">Showcase</a>
            </div>
          </section>

          <section data-auto-animate>
            <h2>React Native 混合开发</h2>
            <h4 class="tl">React Native 应用的结构</h4>
            <div class="g g-half">
              <div style="background-color: white">
                <img src="images/struct.png" alt="" />
              </div>
              <div class="tl">
                <p>
                  从图中可以看到，我们整个的 RN 应用可以分为两层展示：
                  JavaScript Code 层 Native Code 层
                  也可以理解为所谓的应用层和底层。应用层通过 JavaScript 桥接层
                  与底层平台进行交互，获取底层平台的原生 APIs、UI
                  组件及一些自定义组件等。
                </p>
                <p>
                  这样的分层能够使应用层的开发变得简单、高效和跨平台，对于应用的稳定性、运行时的性能来说将和原生平台保持接近。
                </p>
              </div>
            </div>
          </section>
        </section>

        <!-- MARK  -->
        <!-- TODO -->
        <!-- main content start -->

        <section>
          <h1>有时候我们感慨现</h1>
          <h1>在的时光过的真快</h1>
        </section>

        <section>
          <h1 class="">
            我们的生活中总有一些<br />精彩或失落的瞬间&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />值得我们去记录&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
          </h1>
        </section>

        <section>
          <h1>你可以把它当做一款树洞软件</h1>
        </section>

        <section>
          <h1 class="tl">为了它的完美运行，我们在前后端都下了一定的功夫</h1>
          <del style="color: #bbb; opacity: 0.8; font-size: 0.5rem">
            没有 BUG,没有 BUG,没有 BUG
          </del>
        </section>

        <section>
          <h1>前端我们使用 React-Native 混合开发技术</h1>
        </section>

        <section>
          <h1>后端我们使用 Node.JS 开发了 Meet 正常工作所需要的接口</h1>
        </section>

        <section data-auto-animate="">
          <h1>UI</h1>
        </section>
        <section>
          <h1>Android UI</h1>

          <div class="g" style="grid-template-columns: repeat(4, 1fr)">
            <div><img src="./images/android/遇见.png" alt="" /></div>
            <div><img src="./images/android/灵感.png" alt="" /></div>
            <div><img src="./images/android/喜欢.png" alt="" /></div>
            <div><img src="./images/android/数据同步.png" alt="" /></div>
          </div>
        </section>

        <section>
          <h1>iOS UI</h1>

          <div class="g" style="grid-template-columns: repeat(4, 1fr)">
            <div><img src="./images/ios/遇见.png" alt="" /></div>
            <div><img src="./images/ios/灵感.png" alt="" /></div>
            <div><img src="./images/ios/喜欢.png" alt="" /></div>
            <div><img src="./images/ios/数据同步.png" alt="" /></div>
          </div>
        </section>

        <section>
          <h1>Frontend codebase</h1>
        </section>

        <section>
          <h1>Tree</h1>

          <pre><code data-line-numbers="5-12|17-25|28-34" data-trim="">
.
├── App.tsx
├── __tests__
│   └── App-test.tsx
├── android
│   ├── app
│   ├── build.gradle
│   ├── gradle
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   └── settings.gradle
├── app.json
├── babel.config.js
├── configs.ts
├── index.js
├── ios
│   ├── Meet
│   ├── Meet-tvOS
│   ├── Meet-tvOSTests
│   ├── Meet.xcodeproj
│   ├── Meet.xcworkspace
│   ├── MeetTests
│   ├── Podfile
│   └── Podfile.lock
├── metro.config.js
├── package.json
├── src
│   ├── components
│   ├── constants
│   ├── models
│   ├── screens
│   ├── store
│   └── utils
├── tsconfig.json
└── yarn.lock
          </code></pre>
        </section>
        <section>
          <h1>Screen</h1>

          <div class="half-wrapper">
            <div style="width: 23%">
              <div class="r full">
                <div
                  style="
                    position: absolute;
                    width: 80%;
                    height: 20%;
                    top: 40%;
                    left: 0;
                    right: 0;
                    margin: auto;
                    border: 3px solid red;
                  "
                ></div>
                <img src="./images/ios/遇见.png" alt="" />
              </div>
            </div>
            <div style="width: 75%">
              <pre>
                <code data-trim data-noescape data-line-numbers="8-35|68-124|12|82-86" class="tsx">
 &lt;SafeAreaView&gt;
      &lt;View
        style={[
          styles.root,
          { alignItems: 'center', justifyContent: 'center' },
        ]}
      &gt;
        &lt;View&gt;
          &lt;View&gt;
            &lt;Text style={styles.header}&gt;{today}&lt;/Text&gt;
          &lt;/View&gt;
          &lt;View style={styles.content}&gt;
            {data ? (
              &lt;Fragment&gt;
                &lt;Pressable
                  onLongPress={() =&gt; {
                    Clipboard.setString(data.text);
                    toast('已复制: ' + data.text);
                  }}
                &gt;
                  &lt;Text style={styles.text}&gt;{data.text}&lt;/Text&gt;
                &lt;/Pressable&gt;
                {data.from ? (
                  &lt;Text style={{ textAlign: 'right' }}&gt;来自 {data.from}&lt;/Text&gt;
                ) : null}

                {data.author ? (
                  &lt;Text style={{ textAlign: 'right' }}&gt;作者 {data.author}&lt;/Text&gt;
                ) : null}
              &lt;/Fragment&gt;
            ) : (
              &lt;Text&gt;载入中...&lt;/Text&gt;
            )}
          &lt;/View&gt;
        &lt;/View&gt;
        &lt;View style={styles.actions}&gt;
          &lt;TouchableOpacity onPress={handleLike}&gt;
            &lt;Icons
              name=&quot;heart&quot;
              size={18}
              style={[styles.icon, { color: isLiked ? Colors.red : undefined }]}
              solid={isLiked}
            /&gt;
          &lt;/TouchableOpacity&gt;
          &lt;TouchableOpacity onPress={handleShare}&gt;
            &lt;Icons name=&quot;share&quot; size={18} /&gt;
          &lt;/TouchableOpacity&gt;
        &lt;/View&gt;

        &lt;View style={styles.float}&gt;
          &lt;TouchableOpacity
            onPress={() =&gt; {
              animateButton();
              fetchData();
            }}
          &gt;
            &lt;Animatable.View style={styles.refresh_button} ref={buttonRef}&gt;
              &lt;Icons name=&quot;sync&quot; size={16} color={Colors.white} /&gt;
            &lt;/Animatable.View&gt;
          &lt;/TouchableOpacity&gt;
        &lt;/View&gt;
      &lt;/View&gt;
    &lt;/SafeAreaView&gt;


// ...

const styles = StyleSheet.create({
  root: {
    minHeight: Platform.OS === 'ios' ? '95%' : '100%',
    minWidth: '100%',
    flex: 1,
    flexDirection: 'column',
  },
  header: {
    color: '#bbb',
    textAlign: 'center',
  },
  flex: {
    flex: 1,
  },
  content: {
    marginVertical: 12,
    width: '100%',
    paddingHorizontal: 30,
  },
  actions: {
    position: 'absolute',
    bottom: Platform.OS === 'ios' ? 50 : 30,
    left: 0,
    right: 0,
    flexDirection: 'row',
    justifyContent: 'center',
  },
  float: {
    position: 'absolute',
    right: 20,
    top: '75%',
  },
  refresh_button: {
    backgroundColor: Colors.theme,
    padding: 20,
    borderRadius: 50,
    ...(Platform.OS === 'ios'
      ? {
          shadowColor: Colors.deep,
          shadowOffset: {
            width: 0,
            height: 1,
          },
          shadowOpacity: 0.22,
          shadowRadius: 2.22,

          elevation: 3,
        }
      : {}),
  },
  icon: { marginRight: 48 },

  text: {
    color: Colors.theme,
    fontSize: 24,
  },
});

                </code>
              </pre>
            </div>
          </div>
        </section>

        <section>
          <h1>Layout</h1>
          <div class="half-wrapper">
            <div style="width: 30%">
              <div class="r full">
                <img src="./images/ios/喜欢.png" alt="" />
                <div
                  style="bottom: 0; height: 10%; width: 90%"
                  class="mark"
                ></div>
              </div>
            </div>
            <div class="" style="width: 69%">
              <pre><code data-line-numbers="25-78|15-24" data-trim="" data-noescape="" class="tsx">
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import React from 'react';
import { Platform } from 'react-native';
import Icons from 'react-native-vector-icons/FontAwesome5';
import AntdIcons from 'react-native-vector-icons/AntDesign';
import Ionicons from 'react-native-vector-icons/Ionicons';
import { HomeStackScreen } from '../../screens/home';
import { InspireStackScreen } from '../../screens/inspire';
import { FavoriteStackScreen } from '../../screens/favorite';
import { SettingStackScreen } from '../../screens/setting';

const Tab = createBottomTabNavigator();
export const BottomNavigationTabSimpleUsageShowcase = () =&gt; {
  return (
    &lt;Tab.Navigator
      tabBarOptions={{
        keyboardHidesTabBar: true,
        style: { paddingTop: 10 },
        allowFontScaling: true,
        safeAreaInsets: {
          bottom: Platform.OS === 'android' ? 10 : undefined,
        },
      }}
    &gt;
      &lt;Tab.Screen
        name=&quot;Home&quot;
        component={HomeStackScreen}
        options={{
          title: '遇见',

          tabBarIcon: ({ color, size, focused }) =&gt; (
            &lt;Icons
              name=&quot;circle-notch&quot;
              color={color}
              size={size}
              solid={focused}
            /&gt;
          ),
        }}
      /&gt;
      &lt;Tab.Screen
        name=&quot;inspire&quot;
        component={InspireStackScreen}
        options={{
          title: '灵感',
          tabBarIcon: ({ color, size, focused }) =&gt; (
            &lt;Icons name=&quot;lightbulb&quot; color={color} size={size} solid={focused} /&gt;
          ),
        }}
      /&gt;
      &lt;Tab.Screen
        name=&quot;favorite&quot;
        component={FavoriteStackScreen}
        options={{
          title: '喜欢',
          tabBarIcon: ({ color, size, focused }) =&gt; (
            &lt;&gt;
              {focused ? (
                &lt;AntdIcons name=&quot;heart&quot; color={color} size={size} /&gt;
              ) : (
                &lt;AntdIcons name=&quot;hearto&quot; color={color} size={size} /&gt;
              )}
            &lt;/&gt;
          ),
        }}
      /&gt;
      &lt;Tab.Screen
        name=&quot;@me&quot;
        component={SettingStackScreen}
        options={{
          title: '我',
          tabBarIcon: ({ color, size, focused }) =&gt; (
            &lt;&gt;
              &lt;AntdIcons name=&quot;user&quot; color={color} size={size} /&gt;
            &lt;/&gt;
          ),
        }}
      /&gt;
    &lt;/Tab.Navigator&gt;
  );
};
            </code></pre>
            </div>
          </div>
        </section>

        <section>
          <h1>Data Fetching</h1>
          <div class="g g-half">
            <div class="tl">
              <p>Data Fetching 使用了 Axios 库.</p>

              <p>
                Axios 是一个基于 Promise 的 HTTP 库，可以用在浏览器和 Node.JS
                中。
              </p>

              <pre>
<code data-trim="" data-line-numbers="6-9" class="ts">
  const [data, setData] = useState(null);
  useEffect(() => {
    fetchData();
  }, []);

  const fetchData = () => {
    $http.get('/sentences').then((data) => {
      setData(data as any);
    });
  };
</code>

              </pre>
            </div>

            <div class="">
              <pre>
                <code data-line-numbers="" data-trim="" class="ts">
import axios from 'axios';
import camelcaseKeys from 'camelcase-keys';
import { configs } from '../../configs';
import { UserStore } from '../store/user';
import { JSONParseWithDate } from './json-parser';

export const $http = axios.create({
  baseURL: configs.api_url, // 'https://api.innei.ren/hikotoko/v1'
  transformResponse: (res) => {
    return JSONParseWithDate(res);
  },
});

$http.interceptors.request.use((req) => {
  const token = UserStore.token;

  if (token) {
    req.headers.Authorization = token;
  }
  return req;
});
$http.interceptors.response.use(
  (res) => {
    const data = { ...res.data };

    return camelcaseKeys(data, { deep: true });
  },
  (err) => {
    const res = err.response;
    if (!res) {
      return Promise.reject(err);
    }
    if (res?.data?.message) {
      console.log(res.data.message);
    } else if (res?.data) {
      console.log(res.data);
    }
    return Promise.reject(err);
  },
);


                </code>
              </pre>
            </div>
          </div>
        </section>

        <section>
          <h1>State</h1>

          <pre><code data-line-numbers="12|23-31" data-noescape="" class="jsx">
export const HomeScreen = observer(({ navigation }: any) => {
  const today = dayjs().format('YYYY-M-D');

  const [data, setData] = useState(null);
  useEffect(() => {
    fetchData();
  }, []);

  const fetchData = () => {
    $http.get('/sentences').then((data) => {
      setData(data as any);
    });
  };
  // ....
&lt;Fragment&gt;
&lt;Pressable
  onLongPress={() =&gt; {
    Clipboard.setString(data.text);
    toast('已复制: ' + data.text);
  }}
&gt;
  &lt;Text style={styles.text}&gt;{data.text}&lt;/Text&gt;
&lt;/Pressable&gt;
{data.from ? (
  &lt;Text style={{ textAlign: 'right' }}&gt;来自 {data.from}&lt;/Text&gt;
) : null}

{data.author ? (
  &lt;Text style={{ textAlign: 'right' }}&gt;作者 {data.author}&lt;/Text&gt;
) : null}
&lt;/Fragment&gt;
}
          </code></pre>
        </section>

        <section>
          <h1>Store</h1>
          <p>
            State 为了解决单一组件的数据响应式, Store 为了管理整个 App,
            多个组件之间的数据响应式
          </p>
          <p>这里以 Mobx 库为例</p>

          <pre><code data-trim="" data-noescape="" data-line-numbers="3|19-68" class="ts">
export class FavoriteStore {
  constructor() {
    makeAutoObservable(this);
    this.loadList();
  }

  list: FavoriteModel[] = [];

  get map() {
    return new Map(this.list.map((m) => [m.id, m]));
  }

  loadList = async () => {
    const list = await FavoriteStorage.getList();
    this.list = list;
    return this.list;
  };

  addMore = async (
    list: (Omit&lt;FavoriteModel, 'id' | 'type'&gt; &amp;
      Partial&lt;Pick&lt;FavoriteModel, 'id'&gt;&gt;)[],
  ) =&gt; {
    for await (const item of list) {
      await this.add(item);
    }
  };
  add = async (
    model: Omit&lt;FavoriteModel, 'id' | 'type'&gt; &amp;
      Partial&lt;Pick&lt;FavoriteModel, 'id'&gt;&gt;,
  ) =&gt; {
    if (!model.id) {
      model.id = new FlakeId().gen();
      // @ts-ignore
      model.type = SentenceType.USER;
    } else {
      if (
        this.map.has(model.id) ||
        (model.likeId &amp;&amp; this.map.has(model.likeId)) ||
        // @ts-ignore
        this.map.has(model.nonce)
      ) {
        return;
      }

      //@ts-ignore
      model.type = SentenceType.SYSTEM;
      model.likeId = model.id;
    }
    // @ts-ignore
    const newModel = await FavoriteStorage.add(model);
    this.list = this.list.concat(newModel);
    try {
      await $http.post('/sentences', model);
    } catch (e) {}
    return this.list;
  };

  deleteById = async (id: Snowflake) => {
    await FavoriteStorage.delete(id);
    this.list = this.list.filter((m) => m.id !== id);
    return this.list;
  };

  empty = async () => {
    for await (const i of this.list) {
      await this.deleteById(i.id);
    }
  };
}

          </code></pre>
        </section>

        <section>
          <h1>Store</h1>
          <div class="half-wrapper">
            <div style="width: 80%">
              <pre><code data-line-numbers="2-3|9-27" data-noescape="" data-trim="" class="jsx">
export const FavoriteScreen: FC = observer(() =&gt; {
  const { favoriteStore } = useStore();
  const list = favoriteStore.list;

  const navigator = useNavigation();

  return (
    &lt;View style={{ minHeight: '100%' }}&gt;
      {list.length &gt; 0 ? (
        &lt;View style={{ minHeight: '100%', backgroundColor: '#fff' }}&gt;
          &lt;FlatList
            data={list}
            renderItem={(listRenderItem) =&gt; (
              &lt;Fragment&gt;
                &lt;Item
                  item={listRenderItem.item}
                  onDelete={(id) =&gt; {
                    favoriteStore.deleteById(id);
                  }}
                  onPress={(id) =&gt; {
                    navigator.navigate('item-modal', {
                      item: listRenderItem.item,
                    });
                  }}
                /&gt;
                &lt;Divider /&gt;
              &lt;/Fragment&gt;
            )}
          /&gt;
        &lt;/View&gt;
      ) : (
        &lt;View
          style={{
            alignItems: 'center',
            justifyContent: 'center',
            minHeight: '100%',
          }}
        &gt;
          &lt;Text style={{ color: Colors.deep, fontSize: 18 }}&gt;
            你还没有任何喜欢的句子哦 QAQ
          &lt;/Text&gt;
        &lt;/View&gt;
      )}
    &lt;/View&gt;
  );
});


          </code></pre>
            </div>
            <div style="width: 18%" class="ic">
              <div class="r full">
                <div
                  class="mark"
                  style="top: 10%; height: 30%; width: 95%"
                ></div>
                <img src="images/ios/喜欢.png" alt="" />
              </div>
            </div>
          </div>
        </section>

        <section>
          <h1>Storage</h1>
          <pre><code data-line-numbers="4-40|43-49" data-noescape="" data-trim="" class="ts">
class Storage {
  constructor(private readonly key: string) {}

  getList = async (): Promise&lt;FavoriteModel[]&gt; =&gt; {
    try {
      const jsonValue = await AsyncStorage.getItem(this.key);
      return jsonValue !== null ? JSON.parse(jsonValue) : [];
    } catch (e) {
      return [];
    }
  };

  add = async (
    model: Omit&lt;FavoriteModel, 'id'&gt; &amp; Partial&lt;Pick&lt;FavoriteModel, 'id'&gt;&gt;,
  ) =&gt; {
    const { id = new FlakeId().gen() as string } = model;
    const prev = await this.getList();
    const map = new Map(
      prev.map((m) =&gt; {
        return [m.id, m];
      }),
    );
    if (map.has(id)) {
      return prev;
    }
    const newModel = { ...model, id };
    const newList = prev.concat(newModel);
    await AsyncStorage.setItem(this.key, JSON.stringify(newList));
    return newModel;
  };

  delete = async (id: Snowflake) =&gt; {
    if (!id) {
      return;
    }
    const prev = await this.getList();
    const after = prev.filter((m) =&gt; m.id !== id);

    await AsyncStorage.setItem(this.key, JSON.stringify(after));
  };
}

class FavoriteStorageStatic extends Storage {
  public static key = '@meet/favorite';
  constructor() {
    super(FavoriteStorageStatic.key);
  }
}
export const FavoriteStorage = new FavoriteStorageStatic();
          </code></pre>
        </section>

        <section><h1>next section backend</h1></section>

        <section>
          <h1>
            我们为 Meet 开发了这些接口，这些接口驱动着
            Meet，使它的服务能平稳运行。
          </h1>
        </section>

        <section>
          <h1>RESTFul API</h1>
          <table class="tableizer-table">
            <thead>
              <tr class="tableizer-firstrow">
                <th>API</th>
                <th>Description</th>
                <th>Method</th>

                <th>Authorization</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>/v1/auth/register</td>
                <td>用户注册</td>
                <td>POST</td>

                <td>&nbsp;</td>
              </tr>
              <tr>
                <td>/v1/auth/login</td>
                <td>用户登陆接口</td>
                <td>POST</td>

                <td>&nbsp;</td>
              </tr>
              <tr>
                <td>/v1/users</td>
                <td>用户注销</td>
                <td>DELETE</td>

                <td>Bearer Token</td>
              </tr>
              <tr>
                <td>/v1/users</td>
                <td>更新用户信息</td>
                <td>PATCH</td>

                <td>Bearer Token</td>
              </tr>
              <tr>
                <td>/v1/sentences</td>
                <td>随机获取一行句子</td>
                <td>GET</td>
                <td>&nbsp;</td>
                <td>&nbsp;</td>
              </tr>
              <tr>
                <td>/v1/sentences?count=num</td>
                <td>随机获取任意行句子</td>
                <td>GET</td>
                <td>&nbsp;</td>
                <td>&nbsp;</td>
              </tr>
              <tr>
                <td>/v1/sentences</td>
                <td>创建句子</td>
                <td>POST</td>

                <td>Bearer Token</td>
              </tr>
              <tr>
                <td>/v1/sentences/:sentence_id</td>
                <td>更新指定的一行句子</td>
                <td>PATCH</td>

                <td>Bearer Token</td>
              </tr>
              <tr>
                <td>/v1/sentences/:sentence_id</td>
                <td>删除指定的一行句子</td>
                <td>DELETE</td>

                <td>Bearer Token</td>
              </tr>
              <tr>
                <td>/v1/sentences/user</td>
                <td>获取指定用户的句子</td>
                <td>GET</td>

                <td>Bearer Token</td>
              </tr>
              <tr>
                <td>/v1/sentences/like/:sentence_id</td>
                <td>获取喜欢的句子</td>
                <td>GET</td>

                <td>Bearer Token</td>
              </tr>
              <tr>
                <td>/v1/sentences/:sentence_id</td>
                <td>删除用户不喜欢的句子</td>
                <td>DELETE</td>

                <td>Bearer Token</td>
              </tr>
            </tbody>
          </table>
        </section>

        <section>
          <h1>model</h1>

          <pre><code data-trim="" data-line-numbers="1-30|32-61" class="javascript"> 

const SentenceSchema = new mongoose.Schema(
  {
    text: {
      type: String,
    },
    type: {
      type: Number,
      default: SentenceType.USER,
    },
    user_id: {
      type: mongoose.SchemaTypes.ObjectId,
      ref: 'User',
    },
    author: {
      type: String,
    },
    from: {
      type: String,
    },
    nonce: {
      type: String,
    },
    liked_id: {
      type: mongoose.SchemaTypes.ObjectId,
      ref: 'Sentence',
    },
  },
  SchemaOptions,
)


const UserSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true,
    index: 1,
  },
  password: {
    type: String,
    required: true,
    get(val) {
      return val
    },
    set(val) {
      return hashSync(val, 3)
    },
    select: false,
  },
  email: {
    type: String,
    required: true,
  },
  randomCode: {
    type: String,
    default: () => {
      return nanoid(6)
    },
    select: false,
  },
})

          </code></pre>
        </section>

        <section>
          <h1>Sentence Controller</h1>
          <pre><code data-trim="" data-line-numbers="1-234|55-92|56-61|69-78|80-91" class="js">
class SentenceControllerStatic {
  async getRandom(req, res) {
    const { count: queryCount } = req.query
    assert(
      typeof queryCount === 'undefined' ||
        typeof parseInt(queryCount) === 'number',
      422,
      'count must be number or undefined. got ' + queryCount,
    )

    const count = await Sentence.countDocuments()
    assert(count !== 0, 422, 'has no sentences yet.')
    if (!queryCount) {
      const data = await Sentence.findOne().skip(
        Math.floor(Math.random() * count),
      )
      res.send(data)
    } else {
      const data = await Sentence.aggregate([
        {
          $match: {
            liked_id: null,
          },
        },
        {
          $sample: { size: ~~queryCount },
        },
      ])
      res.send(data)
    }
  }

  async insert(req, res) {
    const { author, from, text } = req.body

    validNotEmptyString(text, 'text')
    validStringOrUndefined(author, 'author')
    validStringOrUndefined(from, 'from')

    const { user } = req
    const { _id: user_id } = user

    const data = await Sentence.create({
      author,
      text,
      from,
      type: SentenceType.USER,
      user_id,
      nonce: Snowflake.gen(),
    })

    res.status(201).send(data)
  }

  async likeSentence(req, res) {
    const { id } = req.params

    const { user } = req
    const { _id: user_id } = user

    assert(Types.ObjectId.isValid(id), 422, 'id must be object id. got ' + id)

    const hasDocument = await Sentence.findOne({
      _id: id,
      type: SentenceType.SYSTEM,
    })
    assert(hasDocument, 422, 'sentence not found.')

    const isLiked = await Sentence.findOne({
      type: SentenceType.USER,
      liked_id: id,
    })

    assert(
      !isLiked,
      422,
      'sentence already liked. document id: ' + (isLiked && isLiked._id),
    )

    const newModel = {
      ...hasDocument.toJSON(),
      type: SentenceType.USER,
      liked_id: hasDocument._id,
      user_id,
      nonce: Snowflake.gen(),
    }
    delete newModel._id
    delete newModel.id
    delete newModel.updated_at
    const document = await Sentence.create(newModel)
    res.send(document)
  }

  async patch(req, res) {
    const { id: _id } = req.params

    assert(Types.ObjectId.isValid(_id), 422, 'id must be object id. got ' + _id)
    const { author, from, text } = req.body
    const {
      user: { _id: user_id },
    } = req
    validStringOrUndefined(author, 'author')
    validStringOrUndefined(from, 'from')
    validStringOrUndefined(text, 'text')

    const doc = await Sentence.findOne({
      _id,
      type: SentenceType.USER,
      user_id,
    })
    assert(doc, 422, 'sentence not found.')

    await Sentence.updateOne(
      {
        _id,
        type: SentenceType.USER,
        user_id,
        nonce: Snowflake.gen(),
      },
      {
        author,
        from,
        text,
      },
      {
        omitUndefined: true,
      },
    )

    res.status(204).end()
  }

  async delete(req, res) {
    const { id } = req.params
    const {
      user: { _id: user_id },
    } = req
    assert(id && Types.ObjectId.isValid(id), 422, 'id must be object id.')
    const doc = await Sentence.findOne({
      _id: id,
      type: SentenceType.USER,
      user_id,
    })
    assert(doc, 422, 'sentence not found.')

    await Sentence.deleteOne({ _id: id })
    res.status(204).end()
  }

  async getAllByUser(req, res) {
    // TODO pager
    const { user } = req
    const { _id: user_id } = user

    const data = await Sentence.find({
      type: SentenceType.USER,
      user_id,
    })

    res.send({ data: data })
  }

  // sync sentences
  async syncSentence(req, res) {
    const { body } = req
    const {
      user: { _id: user_id },
    } = req
    assert(Array.isArray(body), 422, `body must be a array, got ${body}`)
    const isValid = body.every(
      (i) =>
        typeof i.nonce === 'string' &&
        typeof i.text === 'string' &&
        [0, 1].includes(i.type),
    )
    assert(
      isValid,
      422,
      `list type must be SentenceType, got ${JSON.stringify(body)}`,
    )

    const now = new Date()
    // console.log(body)
    for await (const item of body) {
      const isBuiltIn = Types.ObjectId.isValid(item.nonce)
      if (isBuiltIn) {
        const sentence = await Sentence.findOne({
          _id: item.nonce,
          type: SentenceType.SYSTEM,
        })

        if (sentence) {
          const model = sentence.toJSON()
          delete model._id
          delete model.created_at
          delete model.updated_at

          model.user_id = user_id
          model.liked_id = sentence._id

          await Sentence.create({
            ...item,
            ...model,
            type: SentenceType.USER,
          })
        } else {
          await Sentence.create({ ...item, user_id })
        }
      } else {
        await Sentence.create({ ...item, user_id })
      }
    }

    // then clean before data

    await Sentence.deleteMany({
      type: SentenceType.USER,
      created_at: {
        $lte: now,
      },
    })

    // get all data

    const list = await Sentence.find({
      type: SentenceType.USER,
      user_id,
    })

    res.send({ data: list })
  }
}

module.exports = new SentenceControllerStatic()

  </code></pre>
        </section>

        <section data-auto-animate="">
          <h1>Authorization</h1>

          <pre><code data-trim="" data-line-numbers="|4|5-6|41-48|7-13|15-17|19|30-37" class="js">
// login

async login(req, res) {
    const { username, password } = req.body
    validNotEmptyString(username, 'username')
    validNotEmptyString(password, 'password')
    const user = await User.findOne({
      username,
    })
      .select('+randomCode +password')
      .lean()

    assert(!!user, 422, 'user not exists')

    const isValid = compareSync(password, user.password)

    assert(isValid, 422, 'password not correct.')

    const token = this.signToken(user)
    delete user.randomCode
    delete user.password

    res.send({
      ...user,
      avatar: getGravatar(user.email),
      token,
    })
  }

  signToken(doc) {
    return (
      'bearer ' +
      sign(
        { username: doc.username, code: doc.randomCode, id: doc._id },
        constant.jwtKey,
      )
    )
  }


  // utils
  const validNotEmptyString = (val, name) => {
  assert(
    typeof val === 'string' && val.length > 0,
    422,
    name + ' must be not empty string, got ' + val,
  )
}
          </code></pre>
        </section>

        <section data-auto-animate="">
          <h1>Authorization</h1>
          <pre><code data-trim="" data-line-numbers="|10-23|17-20|23|25-27,31,29|35-37" class="js">
// middlewares/auth.js
const { verify } = require('jsonwebtoken')
const { User } = require('../models')
/**
 *
 * @param {Express.Request} req
 * @param {Express.Response} res
 * @param {*} next
 */
module.exports = async (req, res, next) => {
  if (req.headers.authorization) {
    const tokenString = req.headers.authorization.replace(/^bearer /, '')

    try {
      const { code, id } = verify(tokenString, constant.jwtKey)

      const user = await User.findById(id).select({ randomCode: 1 })
      const randomCode = user.randomCode

      if (code === randomCode) {
        req.user = user

        return next()
      } else {
        return res.status(422).send({
          message: 'token is expired',
        })
      }
    } catch (e) {
      console.log(e)
      return res.status(422).send({ message: 'token is invalid' })
    }
  }

  return res
    .status(422)
    .send({ message: 'authorization failed, check token please.' })
}

        </code></pre>
        </section>

        <section data-auto-animate="">
          <h1>Authorization</h1>
          <pre><code class="js" data-line-numbers="2,8-14" data-trim="">
// routes/index.js
const authMiddleware = require('../middlewares/auth')

// ...
const sentence = Router()

sentence.get('/', SentenceController.getRandom)
sentence.use(authMiddleware)
sentence.get('/like/:id', SentenceController.likeSentence)
sentence.get('/user', SentenceController.getAllByUser)
sentence.post('/sync', SentenceController.syncSentence)
sentence.post('/', SentenceController.insert)
sentence.patch('/:id', SentenceController.patch)
sentence.delete('/:id', SentenceController.delete)
          </code></pre>
        </section>
        <section>
          <h1>LifeCircle</h1>
          <div id="workflow-1">
            <svg
              style="
                background-color: #bbb;
                font-family: Arial, Helvetica, sans-serif;
              "
              id="SvgjsSvg1006"
              width="891"
              height="388"
              xmlns="http://www.w3.org/2000/svg"
              version="1.1"
              xmlns:xlink="http://www.w3.org/1999/xlink"
              xmlns:svgjs="http://svgjs.com/svgjs"
            >
              <defs id="SvgjsDefs1007">
                <marker
                  id="SvgjsMarker1044"
                  markerWidth="16"
                  markerHeight="12"
                  refX="16"
                  refY="6"
                  viewBox="0 0 16 12"
                  orient="auto"
                  markerUnits="userSpaceOnUse"
                  stroke-dasharray="0,0"
                >
                  <path
                    id="SvgjsPath1045"
                    d="M0,2 L14,6 L0,11 L0,2"
                    fill="#323232"
                    stroke="#323232"
                    stroke-width="2"
                  ></path>
                </marker>
                <marker
                  id="SvgjsMarker1048"
                  markerWidth="16"
                  markerHeight="12"
                  refX="16"
                  refY="6"
                  viewBox="0 0 16 12"
                  orient="auto"
                  markerUnits="userSpaceOnUse"
                  stroke-dasharray="0,0"
                >
                  <path
                    id="SvgjsPath1049"
                    d="M0,2 L14,6 L0,11 L0,2"
                    fill="#323232"
                    stroke="#323232"
                    stroke-width="2"
                  ></path>
                </marker>
                <marker
                  id="SvgjsMarker1052"
                  markerWidth="16"
                  markerHeight="12"
                  refX="16"
                  refY="6"
                  viewBox="0 0 16 12"
                  orient="auto"
                  markerUnits="userSpaceOnUse"
                  stroke-dasharray="0,0"
                >
                  <path
                    id="SvgjsPath1053"
                    d="M0,2 L14,6 L0,11 L0,2"
                    fill="#323232"
                    stroke="#323232"
                    stroke-width="2"
                  ></path>
                </marker>
                <marker
                  id="SvgjsMarker1056"
                  markerWidth="16"
                  markerHeight="12"
                  refX="16"
                  refY="6"
                  viewBox="0 0 16 12"
                  orient="auto"
                  markerUnits="userSpaceOnUse"
                  stroke-dasharray="0,0"
                >
                  <path
                    id="SvgjsPath1057"
                    d="M0,2 L14,6 L0,11 L0,2"
                    fill="#323232"
                    stroke="#323232"
                    stroke-width="2"
                  ></path>
                </marker>
                <marker
                  id="SvgjsMarker1066"
                  markerWidth="16"
                  markerHeight="12"
                  refX="16"
                  refY="6"
                  viewBox="0 0 16 12"
                  orient="auto"
                  markerUnits="userSpaceOnUse"
                  stroke-dasharray="0,0"
                >
                  <path
                    id="SvgjsPath1067"
                    d="M0,2 L14,6 L0,11 L0,2"
                    fill="#323232"
                    stroke="#323232"
                    stroke-width="2"
                  ></path>
                </marker>
                <marker
                  id="SvgjsMarker1076"
                  markerWidth="16"
                  markerHeight="12"
                  refX="16"
                  refY="6"
                  viewBox="0 0 16 12"
                  orient="auto"
                  markerUnits="userSpaceOnUse"
                  stroke-dasharray="0,0"
                >
                  <path
                    id="SvgjsPath1077"
                    d="M0,2 L14,6 L0,11 L0,2"
                    fill="#323232"
                    stroke="#323232"
                    stroke-width="2"
                  ></path>
                </marker>
                <marker
                  id="SvgjsMarker1086"
                  markerWidth="16"
                  markerHeight="12"
                  refX="16"
                  refY="6"
                  viewBox="0 0 16 12"
                  orient="auto"
                  markerUnits="userSpaceOnUse"
                  stroke-dasharray="0,0"
                >
                  <path
                    id="SvgjsPath1087"
                    d="M0,2 L14,6 L0,11 L0,2"
                    fill="#323232"
                    stroke="#323232"
                    stroke-width="2"
                  ></path>
                </marker>
                <marker
                  id="SvgjsMarker1090"
                  markerWidth="16"
                  markerHeight="12"
                  refX="16"
                  refY="6"
                  viewBox="0 0 16 12"
                  orient="auto"
                  markerUnits="userSpaceOnUse"
                  stroke-dasharray="0,0"
                >
                  <path
                    id="SvgjsPath1091"
                    d="M0,2 L14,6 L0,11 L0,2"
                    fill="#323232"
                    stroke="#323232"
                    stroke-width="2"
                  ></path>
                </marker>
                <marker
                  id="SvgjsMarker1094"
                  markerWidth="16"
                  markerHeight="12"
                  refX="16"
                  refY="6"
                  viewBox="0 0 16 12"
                  orient="auto"
                  markerUnits="userSpaceOnUse"
                  stroke-dasharray="0,0"
                >
                  <path
                    id="SvgjsPath1095"
                    d="M0,2 L14,6 L0,11 L0,2"
                    fill="#323232"
                    stroke="#323232"
                    stroke-width="2"
                  ></path>
                </marker>
              </defs>
              <g id="SvgjsG1008" transform="translate(25,158)">
                <path
                  id="SvgjsPath1009"
                  d="M 0 0L 100 0L 100 70L 0 70Z"
                  stroke="rgba(50,50,50,1)"
                  stroke-width="2"
                  fill-opacity="1"
                  fill="#ffffff"
                ></path>
                <g id="SvgjsG1010">
                  <text
                    id="SvgjsText1011"
                    text-anchor="middle"
                    font-size="13px"
                    width="80px"
                    fill="#323232"
                    font-weight="400"
                    align="middle"
                    anchor="middle"
                    size="13px"
                    weight="400"
                    font-style=""
                    opacity="1"
                    y="25.05"
                    transform="rotate(0)"
                  >
                    <tspan id="SvgjsTspan1012" dy="16" x="50">
                      <tspan id="SvgjsTspan1013" style="text-decoration: ">
                        Request
                      </tspan>
                    </tspan>
                  </text>
                </g>
              </g>
              <g id="SvgjsG1014" transform="translate(395,158)">
                <path
                  id="SvgjsPath1015"
                  d="M 0 35C 0 -11.666666666666666 70 -11.666666666666666 70 35C 70 81.66666666666667 0 81.66666666666667 0 35Z"
                  stroke="rgba(50,50,50,1)"
                  stroke-width="2"
                  fill-opacity="1"
                  fill="#ffffff"
                ></path>
                <g id="SvgjsG1016">
                  <text
                    id="SvgjsText1017"
                    text-anchor="middle"
                    font-size="13px"
                    width="50px"
                    fill="#323232"
                    font-weight="400"
                    align="middle"
                    anchor="middle"
                    size="13px"
                    weight="400"
                    font-style=""
                    opacity="1"
                    y="17.05"
                    transform="rotate(0)"
                  >
                    <tspan id="SvgjsTspan1018" dy="16" x="35">
                      <tspan id="SvgjsTspan1019" style="text-decoration: ">
                        Middle
                      </tspan>
                    </tspan>
                    <tspan id="SvgjsTspan1020" dy="16" x="35">
                      <tspan id="SvgjsTspan1021" style="text-decoration: ">
                        ware
                      </tspan>
                    </tspan>
                  </text>
                </g>
              </g>
              <g id="SvgjsG1022" transform="translate(227,158)">
                <path
                  id="SvgjsPath1023"
                  d="M 0 35C 0 -11.666666666666666 70 -11.666666666666666 70 35C 70 81.66666666666667 0 81.66666666666667 0 35Z"
                  stroke="rgba(50,50,50,1)"
                  stroke-width="2"
                  fill-opacity="1"
                  fill="#ffffff"
                ></path>
                <g id="SvgjsG1024">
                  <text
                    id="SvgjsText1025"
                    text-anchor="middle"
                    font-size="13px"
                    width="50px"
                    fill="#323232"
                    font-weight="400"
                    align="middle"
                    anchor="middle"
                    size="13px"
                    weight="400"
                    font-style=""
                    opacity="1"
                    y="25.05"
                    transform="rotate(0)"
                  >
                    <tspan id="SvgjsTspan1026" dy="16" x="35">
                      <tspan id="SvgjsTspan1027" style="text-decoration: ">
                        Router
                      </tspan>
                    </tspan>
                  </text>
                </g>
              </g>
              <g id="SvgjsG1028" transform="translate(589,158)">
                <path
                  id="SvgjsPath1029"
                  d="M 0 35C 0 -11.666666666666666 70 -11.666666666666666 70 35C 70 81.66666666666667 0 81.66666666666667 0 35Z"
                  stroke="rgba(50,50,50,1)"
                  stroke-width="2"
                  fill-opacity="1"
                  fill="#ffffff"
                ></path>
                <g id="SvgjsG1030">
                  <text
                    id="SvgjsText1031"
                    text-anchor="middle"
                    font-size="13px"
                    width="50px"
                    fill="#323232"
                    font-weight="400"
                    align="middle"
                    anchor="middle"
                    size="13px"
                    weight="400"
                    font-style=""
                    opacity="1"
                    y="17.05"
                    transform="rotate(0)"
                  >
                    <tspan id="SvgjsTspan1032" dy="16" x="35">
                      <tspan id="SvgjsTspan1033" style="text-decoration: ">
                        Controll
                      </tspan>
                    </tspan>
                    <tspan id="SvgjsTspan1034" dy="16" x="35">
                      <tspan id="SvgjsTspan1035" style="text-decoration: ">
                        er
                      </tspan>
                    </tspan>
                  </text>
                </g>
              </g>
              <g id="SvgjsG1036" transform="translate(766,158)">
                <path
                  id="SvgjsPath1037"
                  d="M 0 0L 100 0L 100 70L 0 70Z"
                  stroke="rgba(50,50,50,1)"
                  stroke-width="2"
                  fill-opacity="1"
                  fill="#ffffff"
                ></path>
                <g id="SvgjsG1038">
                  <text
                    id="SvgjsText1039"
                    text-anchor="middle"
                    font-size="13px"
                    width="80px"
                    fill="#323232"
                    font-weight="400"
                    align="middle"
                    anchor="middle"
                    size="13px"
                    weight="400"
                    font-style=""
                    opacity="1"
                    y="25.05"
                    transform="rotate(0)"
                  >
                    <tspan id="SvgjsTspan1040" dy="16" x="50">
                      <tspan id="SvgjsTspan1041" style="text-decoration: ">
                        Response
                      </tspan>
                    </tspan>
                  </text>
                </g>
              </g>
              <g id="SvgjsG1042">
                <path
                  id="SvgjsPath1043"
                  d="M659 193L712.5 193L712.5 193L766 193"
                  stroke="#323232"
                  stroke-width="2"
                  fill="none"
                  marker-end="url(#SvgjsMarker1044)"
                ></path>
              </g>
              <g id="SvgjsG1046">
                <path
                  id="SvgjsPath1047"
                  d="M125 193L176 193L176 193L227 193"
                  stroke="#323232"
                  stroke-width="2"
                  fill="none"
                  marker-end="url(#SvgjsMarker1048)"
                ></path>
              </g>
              <g id="SvgjsG1050">
                <path
                  id="SvgjsPath1051"
                  d="M297 193L346 193L346 193L395 193"
                  stroke="#323232"
                  stroke-width="2"
                  fill="none"
                  marker-end="url(#SvgjsMarker1052)"
                ></path>
              </g>
              <g id="SvgjsG1054">
                <path
                  id="SvgjsPath1055"
                  d="M465 193L527 193L527 193L589 193"
                  stroke="#323232"
                  stroke-width="2"
                  fill="none"
                  marker-end="url(#SvgjsMarker1056)"
                ></path>
              </g>
              <g id="SvgjsG1058" transform="translate(589,293)">
                <path
                  id="SvgjsPath1059"
                  d="M 0 35C 0 -11.666666666666666 70 -11.666666666666666 70 35C 70 81.66666666666667 0 81.66666666666667 0 35Z"
                  stroke="rgba(50,50,50,1)"
                  stroke-width="2"
                  fill-opacity="1"
                  fill="#ffffff"
                ></path>
                <g id="SvgjsG1060">
                  <text
                    id="SvgjsText1061"
                    text-anchor="middle"
                    font-size="13px"
                    width="50px"
                    fill="#323232"
                    font-weight="400"
                    align="middle"
                    anchor="middle"
                    size="13px"
                    weight="400"
                    font-style=""
                    opacity="1"
                    y="25.05"
                    transform="rotate(0)"
                  >
                    <tspan id="SvgjsTspan1062" dy="16" x="35">
                      <tspan id="SvgjsTspan1063" style="text-decoration: ">
                        Model
                      </tspan>
                    </tspan>
                  </text>
                </g>
              </g>
              <g id="SvgjsG1064">
                <path
                  id="SvgjsPath1065"
                  d="M624 293L624 260.5L624 260.5L624 228"
                  stroke="#323232"
                  stroke-width="2"
                  fill="none"
                  marker-end="url(#SvgjsMarker1066)"
                ></path>
              </g>
              <g id="SvgjsG1068" transform="translate(395,288)">
                <path
                  id="SvgjsPath1069"
                  d="M 0 35C 0 -11.666666666666666 70 -11.666666666666666 70 35C 70 81.66666666666667 0 81.66666666666667 0 35Z"
                  stroke="rgba(50,50,50,1)"
                  stroke-width="2"
                  fill-opacity="1"
                  fill="#ffffff"
                ></path>
                <g id="SvgjsG1070">
                  <text
                    id="SvgjsText1071"
                    text-anchor="middle"
                    font-size="13px"
                    width="50px"
                    fill="#323232"
                    font-weight="400"
                    align="middle"
                    anchor="middle"
                    size="13px"
                    weight="400"
                    font-style=""
                    opacity="1"
                    y="25.05"
                    transform="rotate(0)"
                  >
                    <tspan id="SvgjsTspan1072" dy="16" x="35">
                      <tspan id="SvgjsTspan1073" style="text-decoration: ">
                        Auth
                      </tspan>
                    </tspan>
                  </text>
                </g>
              </g>
              <g id="SvgjsG1074">
                <path
                  id="SvgjsPath1075"
                  d="M430 288L430 258L430 258L430 228"
                  stroke="#323232"
                  stroke-width="2"
                  fill="none"
                  marker-end="url(#SvgjsMarker1076)"
                ></path>
              </g>
              <g id="SvgjsG1078" transform="translate(465,25)">
                <path
                  id="SvgjsPath1079"
                  d="M 0 34.5C 0 -11.5 138 -11.5 138 34.5C 138 80.5 0 80.5 0 34.5Z"
                  stroke="rgba(50,50,50,1)"
                  stroke-width="2"
                  fill-opacity="1"
                  fill="#ffffff"
                ></path>
                <g id="SvgjsG1080">
                  <text
                    id="SvgjsText1081"
                    text-anchor="middle"
                    font-size="13px"
                    width="118px"
                    fill="#323232"
                    font-weight="400"
                    align="middle"
                    anchor="middle"
                    size="13px"
                    weight="400"
                    font-style=""
                    opacity="1"
                    y="24.55"
                    transform="rotate(0)"
                  >
                    <tspan id="SvgjsTspan1082" dy="16" x="69">
                      <tspan id="SvgjsTspan1083" style="text-decoration: ">
                        Error Handling
                      </tspan>
                    </tspan>
                  </text>
                </g>
              </g>
              <g id="SvgjsG1084">
                <path
                  id="SvgjsPath1085"
                  d="M430 158C 434.2571989578309 109.33999324873204 534 142.84588007191599 534 94"
                  stroke="#323232"
                  stroke-width="2"
                  fill="none"
                  marker-end="url(#SvgjsMarker1086)"
                ></path>
              </g>
              <g id="SvgjsG1088">
                <path
                  id="SvgjsPath1089"
                  d="M624 158C 627.8500352649098 113.9938955546057 534 138.17420061529128 534 94"
                  stroke="#323232"
                  stroke-width="2"
                  fill="none"
                  marker-end="url(#SvgjsMarker1090)"
                ></path>
              </g>
              <g id="SvgjsG1092">
                <path
                  id="SvgjsPath1093"
                  d="M603 59.5C 695.4429758250808 75.80019081494217 816 64.13094226530234 816 158"
                  stroke="#323232"
                  stroke-width="2"
                  fill="none"
                  marker-end="url(#SvgjsMarker1094)"
                ></path>
              </g>
            </svg>
          </div>
        </section>
        <section>
          <h1>拥抱开源</h1>
          <p>此项目以开源于 GitHub</p>
          <p>
            <a href="https://github.com/Innei/meet-rn"> meet-rn </a>
            <span> (https://github.com/Innei/meet-rn) </span>
          </p>
          <p>
            <a href="https://github.com/Innei/hikotoko-server-demo">
              hikotoko-server
            </a>
            <span> (https://github.com/Innei/hikotoko-server-demo) </span>
          </p>
        </section>

        <section>
          <div style="position: relative">
            <h1 class="fragment fade-out">生活的每一瞬间，都值得遇见</h1>
            <h1 class="fragment fade-in ab" style="left: 0; right: 0; top: 0">
              感谢聆听
            </h1>
          </div>
        </section>

        <section>
          <h2>Acknowledge</h2>

          <ul>
            <li>
              <a href="https://juejin.cn/post/6844903913427206152"
                >React Native 混合开发与实现
              </a>
            </li>
            <li>
              <a
                href="https://searchsoftwarequality.techtarget.com/definition/hybrid-application-hybrid-app"
                >What is a hybrid application</a
              >
            </li>
            <li>
              <a href="https://zhuanlan.zhihu.com/p/105556638">
                混合开发到底怎么个混法？</a
              >
            </li>
          </ul>
        </section>
      </div>
    </div>
  </body>
  <link rel="stylesheet" href="./script/plugin/highlight/zenburn.css" />

  <script src="./index.js"></script>
</html>
